热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

时效性|事实_SSO轻量级实现指南(原生Java实现):SSOServer部分

篇首语:本文由编程笔记#小编为大家整理,主要介绍了SSO轻量级实现指南(原生Java实现):SSOServer部分相关的知识,希望对你有一定的参考价值。OAut

篇首语:本文由编程笔记#小编为大家整理,主要介绍了SSO 轻量级实现指南(原生 Java 实现):SSO Server 部分相关的知识,希望对你有一定的参考价值。


OAuth 是当前单点登录(SSO)和用户授权的标准协议——现在就让我们一起动手撸一个 SSO 的实现吧!

源码在:



  • SSO 中心 https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-sso
  • SSO Client 客户端 https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-sso-client

我们开源的特色:


  • 轻量级,代码行数少
  • 除了 JVM 和 Spring 没啥依赖,尽量原生,基本没什么第三方引用库
  • 代码风格务求清晰、简洁易维护、干净

SSO Server 即 SSO 中心,负责统一用户认证的。另外有 SSO Client 部分,我们另起文章再讲。


SSO 与 OAuth 傻傻分不清?

开始之前先说说废话(之所以说废话的原因是,其实你可以无视这段概念性的介绍,直接开撸)。

OAuth 是 OAuth,OAuth 不单单为 SSO 服务的。OAuth 协议初衷是为了用户不用告诉第三方系统账号和密码就可以访问受限的资源,——可以成为 SSO 的通行协议这个想必原设计者都没有料到的。没有 OAuth 之前,SSO 老早就有,只是各家各法自行实现,总能达到单点登录的目的。也就是说,SSO 的协议不一定是 OAuth,而 OAuth 不一定服务于 SSO。

SSO 与 OAuth 都是紧扣“我是谁”之要义,即用户身份认证的问题,这也是核心的问题,所有关于用户一切的信息都应由 SSO 中心或 OAuth 资源服务器所把控。稍有出入的是 OAuth 认证服务器往往是与资源服务器在一起的,这个一起的意思可以是物理意义上的同一部机器。但 SSO 中心呢?一般简单、纯粹的得多,就是做用户认证的,——即使涉及用户权限的 SSO 中心,顶多也是功能性的、系统级的权限控制,而不是垂直的数据权限控制(资源的权限控制)。也就是说,SSO 中心不负责资源问题,而资源往往在客户端 Client 那边。总之,狭义的 OAuth 很可能是整个大系统中,对外服务的一个模块;而 SSO 中心则纯粹得多,通常独立部署,独立服务,只做好 SSO 一件事情。

在流程上,SSO 与 OAuth 也有显著不同,例如同意授权访问,典型的第三方登录是有这一步的(如下图所示),但 SSO 没有吧?


SSO 流程

SSO 流程如下(借图,来自这里)


用户登录/注销

登录 Login

当前是使用账号密码登录,未来也应该支持如微信、微博的第三方登录。登录的作于在于识别“我是谁”的目的,在 SSO 中心标识某某用户已经是在登录的状态,以实现“单点登录”。具体说,会产生关键的标识状态 Session 和浏览器 COOKIEs。Session 仍是记忆登录状态的重要信息,否则后面获取 Token 就无法进行(因为不知道哪个用户!)。

登录控制器 LoginController 源码在这里,关键的 Service 部分在这里。

登录成功或失败一般允许指 redirectUrl,但我们没有,因为当前这登录接口是跨域的,界面完全由客户端指定,所以客户端自己控制就好。不过感觉上登录界面放在 SSO 中心会安全一点吧,毕竟允许跨域了。


注销

当前注销只是清空 session 而已,但实际 SSO 复杂得多,理论上某个应用注销了,其他所有已登录的应用也有要同步注销。这部分暂且不表,待后面补充。


注册

注册分为用户注册和客户端注册。


用户注册

用户注册没什么好说的,常规流程的逻辑,参见源码。


客户端注册

接入的客户端有时也称“应用”。客户端模型如下面 SQL 所示。clientId 有时也称 appIdappKeyclientSecret 是密钥,但跟密码的意思没什么区别,肯定不能外泄出去。

CREATE TABLE `auth_client_details` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键 id,自增',
`name` VARCHAR(20) NOT NULL COMMENT '客户端名称' COLLATE 'utf8mb4_bin',
`content` VARCHAR(256) NULL DEFAULT NULL COMMENT '简介' COLLATE 'utf8mb4_bin',
`clientId` VARCHAR(100) NOT NULL COMMENT '接入的客户端ID' COLLATE 'utf8mb4_bin',
`clientSecret` VARCHAR(255) NOT NULL COMMENT '接入的客户端的密钥' COLLATE 'utf8mb4_bin',
`redirecUri` VARCHAR(1000) NULL DEFAULT NULL COMMENT '回调地址' COLLATE 'utf8mb4_bin',
`stat` TINYINT(4) NULL DEFAULT NULL COMMENT '数据字典:状态',
`uid` BIGINT(20) NULL DEFAULT NULL COMMENT '唯一 id,通过 uuid 生成不重复 id',
`extend` TEXT NULL DEFAULT NULL COMMENT '扩展 JSON 字段' COLLATE 'utf8mb4_bin',
`tenantId` INT(11) NULL DEFAULT NULL COMMENT '租户 id',
`creator` INT(11) NULL DEFAULT NULL COMMENT '创建者 id',
`createDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '也是注册时间',
`updateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
)
COMMENT='接入的客户端信息表'
COLLATE='utf8mb4_bin'

clientIdclientSecret 都是随机字符串生成的,详见下面创建 client_details 部分。

@RestController
@RequestMapping("/oauth")
public class OauthController implements SsoDAO
/**
* 注册需要接入的客户端信息
*
* @param client
* @return
*/

@PostMapping(value = "/clientRegister", produces = MediaType.APPLICATION_JSON_VALUE)
public String clientRegister(@RequestParam ClientDetails client)
if (!StringUtils.hasText(client.getName()))
throw new IllegalArgumentException("客户端的名称和回调地址不能为空");
String clientId = StrUtil.getRandomString(24);// 生成24位随机的 clientId
ClientDetails savedClientDetails = findClientDetailsByClientId(clientId);
// 生成的 clientId 必须是唯一的,尝试十次避免有重复的 clientId
for (int i &#61; 0; i < 10; i&#43;&#43;)
if (savedClientDetails &#61;&#61; null)
break;
else
clientId &#61; StrUtil.getRandomString(24);
savedClientDetails &#61; findClientDetailsByClientId(clientId);


client.setClientId(clientId);
client.setClientSecret(StrUtil.getRandomString(32));
// 保存到数据库
return ClientDetailDAO.create(client) &#61;&#61; null ? BaseController.jsonNoOk() : BaseController.jsonOk();

……


SSO 登录

你以为上面用户登录就完事了&#xff1f;只是完成了三分之一&#xff0c;完整的单点登录还有其余的 66.6666……% ——我们接着看。


获取授权码

为什么要获取授权码&#xff08;Authorization Code&#xff09;&#xff0c;不能直接返回 Token 吗&#xff1f;因水平所限我也不太清楚&#xff0c;好像为了安全性吧&#xff0c;好像 OAuth 有其他模式不用授权码的&#xff1f;我没去管了&#xff0c;反正最主流就是授权码模式。不懂得看官请琢磨上面的流程图&#xff0c;或者先去消化 OAuth 的机制。

获取授权码接口所需的参数参见 SsoController 控制器的方法&#xff0c;源码这里。

&#64;Autowired
private AuthorizationService authService;
/**
* 获取 Authorization Code
*
* &#64;param client_id 客户端 ID
* &#64;param redirect_uri 回调 URL
* &#64;param scope 权限范围
* &#64;param status 用于防止CSRF攻击&#xff08;非必填&#xff09;
* &#64;param req 请求对象
* &#64;return
*/

&#64;RequestMapping(value &#61; "/authorize_code", produces &#61; BaseController.JSON)
public Object authorize(&#64;RequestParam(required &#61; true) String client_id,
// &#64;formatter:off
&#64;RequestParam(required &#61; true) String redirect_uri,
&#64;RequestParam(required &#61; false) String scope,
&#64;RequestParam(required &#61; false) String status,
HttpServletRequest req)
// &#64;formatter:on
LOGGER.info("获取 Authorization Code");
User loginedUser &#61; null;
try
loginedUser &#61; UserUtils.getLoginedUser(req);
catch (Throwable e)
LOGGER.warning(e);
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_CLIENT);

// 生成 Authorization Code
String authorizationCode &#61; authService.createAuthorizationCode(client_id, scope, loginedUser);
String params &#61; "?code&#61;" &#43; authorizationCode;
if (StringUtils.hasText(status))
params &#43;&#61; "&status&#61;" &#43; status;
return new ModelAndView("redirect:" &#43; redirect_uri &#43; params);

据此我们了解几个事实。


  • 只有用户登录了&#xff0c;才有对应的授权码。UserUtils.getLoginedUser(req); 这句从 Session 返回已登录的用户信息。
  • 用户哪个浏览器登录&#xff0c;就在哪个浏览器获取授权码&#xff0c;不然就是未登录状态。
  • 该接口只能前端调用
  • 该接口返回 HTTP 304 重定向&#xff0c;携带 code 参数&#xff08;就是授权码&#xff09;跳转到 redirect_uri。就是说该接口不会返回什么 JSON。

状态码有时效性&#xff0c;一般十分钟&#xff0c;而且是一次性的&#xff0c;用完了要销毁。


客户端接入 SSO 之第一步

从原理上讲&#xff0c;这也是客户端服务接入 SSO 的第一步&#xff08;当然我们会提供一个封装好的 SDK&#xff0c;对于调用者是屏蔽细节的&#xff09;。用户成功登录后&#xff0c;已在 SSO 中心留存有 COOKIEs 的登录信息&#xff0c;于是其他第三方应用可以访问 SSO 中心获取用户信息&#xff08;当然不是直接获取&#xff0c;而且先要获取授权码&#xff09;。

客户端可以通过授权码获取 AccessToken&#xff0c;然后再根据 AccessToken 获取用户信息&#xff0c;完成本地登录。总之我们提到了两次登录验证&#xff1a;第一次是用户身份验证&#xff08;用户凭用户名和密码可以登录&#xff09;&#xff1b;第二次是客户端认证&#xff08;客户端凭 id 和密钥再结合用户信息&#xff08;授权码&#xff09;去登录&#xff09;&#xff0c;这部分我们下面小结会详细讲。


生成授权码原理

进入 authService.createAuthorizationCode() 源码我们看看如何生成授权码。

/**
* 根据 clientId、scope 以及当前时间戳生成 AuthorizationCode&#xff08;有效期为10分钟&#xff09;
*
* &#64;param clientId 客户端ID
* &#64;param scope
* &#64;param user 用户信息
* &#64;return
*/

public String createAuthorizationCode(String clientId, String scope, User user)
if (!StringUtils.hasText(scope))
scope &#61; "DEFAULT_SCOPE";
// 1. 拼装待加密字符串&#xff08;clientId &#43; scope &#43; 当前精确到毫秒的时间戳&#xff09;
String str &#61; clientId &#43; scope &#43; String.valueOf(System.currentTimeMillis());
// 2. SHA1加密
String encryptedStr &#61; Digest.getSHA1(str);
int timeout &#61; ExpireEnum.AUTHORIZATION_CODE.getTime() * 60;
// 3.1 保存本次请求的授权范围
ExpireCache.CACHE.put(encryptedStr &#43; ":scope", scope, timeout);
// 3.2 保存本次请求所属的用户信息
ExpireCache.CACHE.put(encryptedStr &#43; ":user", user, timeout);
// 4. 返回Authorization Code
return encryptedStr;

主要是这么几步&#xff1a;1. 拼装待加密字符串&#xff08;clientId &#43; scope &#43; 当前精确到毫秒的时间戳&#xff09;&#xff1b;2. SHA1 加密&#xff1b;3. 保存到缓存&#xff08;不用保存在数据库&#xff09;。

带过期时间的缓存大家想到的是 Redis&#xff0c;但我这里用了 JVM 的缓存&#xff0c;就是自己写的 Map&#xff0c;无他&#xff0c;懒得部署 Redis 了……


客户端认证&#xff08;颁发 AccessToken&#xff09;

客户端认证的过程就是颁发 AccessToken。我们看看客户端认证的源码定义&#xff0c;需要哪些参数。

/**
* 通过 Authorization Code 获取 Access Token
*
* &#64;param client_id 客户端 id
* &#64;param client_secret 接入的客户端的密钥
* &#64;param code 前面获取的 Authorization Code
* &#64;param grant_type 授权方式
* &#64;param request 请求对象
* &#64;return
*/

&#64;RequestMapping("/authorize")
public String issue(&#64;RequestParam(required &#61; true) String client_id,
// &#64;formatter:off
&#64;RequestParam(required &#61; true) String client_secret,
&#64;RequestParam(required &#61; true) String code,
&#64;RequestParam(required &#61; true) String grant_type,
HttpServletRequest request)
// &#64;formatter:on
LOGGER.info("通过 Authorization Code 获取 Access Token");
// 校验授权方式
if (!GrantTypeEnum.AUTHORIZATION_CODE.getType().equals(grant_type))
return SsoUtil.oauthError(ErrorCodeEnum.UNSUPPORTED_GRANT_TYPE);
ClientDetails savedClientDetails &#61; findClientDetailsByClientId(client_id);
// 校验请求的客户端秘钥和已保存的秘钥是否匹配
if (!(savedClientDetails !&#61; null && savedClientDetails.getClientSecret().equals(client_secret)))
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_CLIENT);
String scope &#61; ExpireCache.CACHE.get(code &#43; ":scope", String.class);
User user &#61; ExpireCache.CACHE.get(code &#43; ":user", User.class);
// 如果能够通过 Authorization Code 获取到对应的用户信息&#xff0c;则说明该 Authorization Code 有效
if (StringUtils.hasText(scope) && user !&#61; null)
// 过期时间
Long expiresIn &#61; LocalDateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
// 生成 Access Token
String accessTokenStr &#61; authService.createAccessToken(user, savedClientDetails, grant_type, scope, expiresIn);
// 查询已经插入到数据库的 Access Token
AccessToken authAccessToken &#61; AcessTokenDAO.setWhereQuery("accessToken", accessTokenStr).findOne();
// 生成 Refresh Token
String refreshTokenStr &#61; authService.createRefreshToken(user, authAccessToken);
IssueToken token &#61; new IssueToken(); // 返回数据
token.setAccess_token(authAccessToken.getAccessToken());
token.setRefresh_token(refreshTokenStr);
token.setExpires_in(expiresIn);
token.setScope(authAccessToken.getScope());
return JsonHelper.toJson(token);
else
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_GRANT);

据此我们了解几个事实。


  • 该接口只能服务端调用。客户端密钥保存在服务端&#xff0c;不应暴露给前端。故所以认证客户端务必在服务端完成&#xff0c;即后台来通讯请求。
  • 进入该接口&#xff0c;要判断密钥是否正确
  • 授权码相当于获取缓存中的 key&#xff0c;value 就是用户信息
  • client 和 user 没问题之后&#xff0c;可以创建 AccessToken
  • AccessToken 保存到数据库。如果已有则再更新。
  • 还生成 RefreshToken&#xff0c;这将会后面讲
  • 这个 AccessToken 外表一堆字符串&#xff0c;实际蕴含什么意思呢&#xff1f;Token 不是密码但胜似密码&#xff0c;他内部包含了不仅用户信息还有客户端的信息&#xff0c;故 AccessToken &#61; 用户&#43;客户端&#xff08;应用&#xff09;的信息

至此就完成了登录了&#xff0c;进度……100%。

至于生成 Token 原理大家可以进入 Service 相关代码浏览&#xff0c;大致都是 SHA1 加密之类的&#xff0c;这里不再赘述。

实际设计中有两点“最佳实践”分享给大家。


  • 虽然有份“获取授权码”和“客户端认证”两个接口两个步骤&#xff0c;但前端一次请求就可以搞定了&#xff0c;这是在 SSO_Client 前端执行的。
  • 单纯返回 AccessToken 之外&#xff0c;最好还要返回用户的详细信息&#xff0c;不然又要前端请求多次。当然标准的 OAuth 没要求返回用户信息。不过目前我还去实现……有时间就搞

刷新 AccessToken-----> RefreshToken

一般 Token 时效性。


  • AccessToken&#xff0c;三十天
  • RefreshToken 365 日

当然&#xff0c;根据你的场景调整。

逻辑大同小异&#xff0c;我们直接贴代码。

/**
* 通过 Refresh Token 刷新 Access Token
*
* &#64;param refresh_token
* &#64;return
*/

&#64;RequestMapping(value &#61; "/refreshToken", produces &#61; MediaType.APPLICATION_JSON_VALUE)
public String refreshToken(&#64;RequestParam(required &#61; true) String refresh_token)
LOGGER.info("通过 Refresh Token 刷新 Access Token");
RefreshToken authRefreshToken &#61; RefreshTokenDAO.setWhereQuery("refreshToken", refresh_token).findOne();
if (authRefreshToken &#61;&#61; null)
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_GRANT);
// 如果 Refresh Token 已经失效&#xff0c;则需要重新生成
if (SsoUtil.checkIfExpire(authRefreshToken))
return SsoUtil.oauthError(ErrorCodeEnum.EXPIRED_TOKEN);
// 获取存储的 Access Token
AccessToken authAccessToken &#61; AcessTokenDAO.findById(authRefreshToken.getTokenId());
// 获取对应的客户端信息
ClientDetails savedClientDetails &#61; ClientDetailDAO.findById(authAccessToken.getClientId());
// 获取对应的用户信息
User user &#61; UserCommonDAO.UserDAO.findById(authAccessToken.getUserId());
// 新的过期时间
Long expiresIn &#61; LocalDateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
// 生成新的 Access Token
String newAccessTokenStr &#61; authService.createAccessToken(user, savedClientDetails, authAccessToken.getGrantType(), authAccessToken.getScope(), expiresIn);
IssueToken token &#61; new IssueToken(); // 返回数据
token.setAccess_token(newAccessTokenStr);
token.setRefresh_token(refresh_token);
token.setExpires_in(expiresIn);
token.setScope(authAccessToken.getScope());
return JsonHelper.toJson(token);

有些厂家不是这么 RefreshToken 的&#xff0c;它是使用基本认证的方式验证客户端身份&#xff0c;如 Authorization: Basic $Base64.encode(clientId&#43;":"&#43;clientSecret)。可见它只需要客户端信息&#xff0c;不需要用户信息。

AccesToken 和 RefreshToken 怎么用呢&#xff1f;这就要看我们 SSO Client 如何调用了&#xff0c;——下篇文章再为大家介绍。


小结

SSO 中心没有想象中的难&#xff0c;当然还有其他周边的问题&#xff0c;如安全性的问题&#xff0c;或者用户权限那部分&#xff0c;会越做越复杂的。不管怎么样只要方向路线正确&#xff0c;那么干就是了&#xff01;

推荐参考文章


  • OAuth2.0协议入门 ——非常不错&#xff0c;我也是参考其代码实现&#xff0c;它教会了我许多&#xff01;
  • SSO 开源实现 Kisso
  • 基于 OAuth 2 的 smart-sso
  • XXL-SSO
  • IAM&#xff1a;MaxKey 国内开源IAM第一品牌
  • 旧帖《新浪微博如何实现 SSO 的分析》
  • 单点登录跨域iframe互相通信方案

推荐阅读
  • Servlet多用户登录时HttpSession会话信息覆盖问题的解决方案
    本文讨论了在Servlet多用户登录时可能出现的HttpSession会话信息覆盖问题,并提供了解决方案。通过分析JSESSIONID的作用机制和编码方式,我们可以得出每个HttpSession对象都是通过客户端发送的唯一JSESSIONID来识别的,因此无需担心会话信息被覆盖的问题。需要注意的是,本文讨论的是多个客户端级别上的多用户登录,而非同一个浏览器级别上的多用户登录。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
  • Cookie和Session的联系Cookie是在客户端保持状态,Session是在服务器端保持状态。由于服务器端保持状态在客户端也需要保存一个标识,所以Session需要借助于 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • LVS实现负载均衡的原理LVS负载均衡负载均衡集群是LoadBalance集群。是一种将网络上的访问流量分布于各个节点,以降低服务器压力,更好的向客户端 ... [详细]
  • 一.常见基于身份识别进行反爬1通过headers字段来反爬headers中有很多字段,这些字段都有可能会被对方服务器拿过来进行判断是否为爬虫1.1通过headers中的User-A ... [详细]
  • 浏览器如何工作(How browsers work)的阅读笔记
    浏览器如何工作(Howbrowserswork)的阅读笔记1.整体结构完整的浏览器整体框架的发改如下:UI:就是那些我们常常 ... [详细]
author-avatar
zhang时代小窝
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有